﻿//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: QueryUtilityMethodHandler.cs
//
//  Description: Converts methods to SQL compatible strings.
// 
//--------------------------------------------------------------------------

using System;
using System.Linq.Expressions;
using System.Reflection;
using Chili.Opf3.Mapping;
using System.Text;

namespace Chili.Opf3.Linq
{
	/// <summary>
	/// This class converts the methods on <see cref="QueryUtility">QueryUtility</see>
	/// into strings that are SQL compatible and can be executed on the storage 
	/// as part of a SQL query.
	/// </summary>
	public sealed class QueryUtilityMethodsHandler : IMethodHandler
	{
		#region IMethodHandler Members

		/// <summary>
		/// Returns whether the class can handle the given method.
		/// </summary>
		/// <param name="method">The method to handle.</param>
		/// <param name="type">The type on which the method is defined.</param>
		public bool CanHandle(MethodInfo method, Type type)
		{
			// Check, if we support the send in methods.
			if (type == typeof(QueryUtility))
			{
				switch (method.Name)
				{
					case "Max":
					case "Min":
					case "Count":
					case "Field":
					case "Between":
					case "Overlaps":
					case "In":
						return true;
				}
			}
			return false;
		}

		/// <summary>
		/// Returns the method call converted in an SQL compatible string.
		/// </summary>
		/// <param name="expression">The expression that is converted to the SQL compatible string. This expression
		/// contains the method that has been called with all the arguments and eventually sub expressions.</param>
		/// <param name="args">Holds a utility class that contains different methods to be used during string compilation.</param>
		public string Handle(MethodCallExpression expression, MethodHandlerArgs args)
		{
			// Convert the given methods into an SQL string.
			switch (expression.Method.Name)
			{
				case "Max":
					return string.Format(" max({0})", args.ProcessExpression(expression.Arguments[0]));
				case "Min":
					return string.Format(" min({0})", args.ProcessExpression(expression.Arguments[0]));
				case "Count":
					return string.Format(" count({0})", args.ProcessExpression(expression.Arguments[0]));
				case "Field":
					return HandleFieldMethod(expression, args);
				case "Between":
					return HandleBetweenMethod(expression, args);
				case "Overlaps":
					return HandleOverlapsMethod(expression, args);
				case "In":
					return HandleInMethod(expression, args);
				default:
					throw new NotImplementedException("Method is not implemented: " + expression.Method.Name);
			}
		}

		#endregion

		/// <summary>
		/// Handles the In method.
		/// </summary>
		/// <param name="expression">The expression that is converted to the SQL compatible string. This expression
		/// contains the method that has been called with all the arguments and eventually sub expressions.</param>
		/// <param name="args">Holds a utility class that contains different methods to be used during string compilation.</param>
		private string HandleInMethod(MethodCallExpression expression, MethodHandlerArgs args)
		{
			var expr = expression.Arguments[1];
			string parameters = null;

			if (expr is NewArrayExpression)
			{
				// just let the builder handle the expression.
				parameters = args.ProcessExpression(expr);
			}
			else if (expr is MemberExpression)
			{
				MemberExpression mexpr = (MemberExpression)expr;
				ConstantExpression cexpr = (ConstantExpression)mexpr.Expression;

				// check if we got an instance that we can use to invoke.
				object instance = cexpr.Value != null ? cexpr.Value : null;
				Array value = null;

				// check if we can get the value from the property or field.
				PropertyInfo prop = mexpr.Member as PropertyInfo;
				if (prop != null)
					value = (Array)prop.GetValue(instance, null);
				FieldInfo field = mexpr.Member as FieldInfo;
				if (field != null)
					value = (Array)field.GetValue(instance);
				else
					throw new NotImplementedException("Other types than methods, fields and properties can't be evaluated by the builder.");

				// create the parameters from the array.
				parameters = CreateParametersFromArray(value, args);
			}

			return string.Format("{0} in {1}", args.ProcessExpression(expression.Arguments[0]), parameters);
		}

		/// <summary>
		/// Handles the overlaps method.
		/// </summary>
		/// <param name="expression">The expression that is converted to the SQL compatible string. This expression
		/// contains the method that has been called with all the arguments and eventually sub expressions.</param>
		/// <param name="args">Holds a utility class that contains different methods to be used during string compilation.</param>
		private string HandleOverlapsMethod(MethodCallExpression expression, MethodHandlerArgs args)
		{
			string[] arguments = new string[4];
			for (int i = 0; i < arguments.Length; i++)
			{
				NewExpression expr = expression.Arguments[i] as NewExpression;
				arguments[i] = (expr != null) ? args.AddParameter(InvokeNewExpression<DateTime>(expr), typeof(DateTime))
					: args.ProcessExpression(expression.Arguments[i]);
			}

			return string.Format("({0} < {1} and {2} > {3})", arguments[0], arguments[3], arguments[1], arguments[2]);
		}

		/// <summary>
		/// Handles the between method.
		/// </summary>
		/// <param name="expression">The expression that is converted to the SQL compatible string. This expression
		/// contains the method that has been called with all the arguments and eventually sub expressions.</param>
		/// <param name="args">Holds a utility class that contains different methods to be used during string compilation.</param>
		private static string HandleBetweenMethod(MethodCallExpression expression, MethodHandlerArgs args)
		{
			// Get the SQL for the field.
			string field = args.ProcessExpression(expression.Arguments[0]);
			// Invoke the new expresison to the get values.
			DateTime date1 = InvokeNewExpression<DateTime>((NewExpression)expression.Arguments[1]);
			DateTime date2 = InvokeNewExpression<DateTime>((NewExpression)expression.Arguments[2]);

			// Create the SQL statement.
			return string.Format("(({0} >= {1}) and ({0} <= {2}))", field,
				args.AddParameter(date1, typeof(DateTime)), args.AddParameter(date2, typeof(DateTime)));
		}

		/// <summary>
		/// Handles the "Field" method.
		/// </summary>
		/// <param name="expression">The expression that is converted to the SQL compatible string. This expression
		/// contains the method that has been called with all the arguments and eventually sub expressions.</param>
		/// <param name="args">Holds a utility class that contains different methods to be used during string compilation.</param>
		private static string HandleFieldMethod(MethodCallExpression expression, MethodHandlerArgs args)
		{
			ConstantExpression c1 = (ConstantExpression)expression.Arguments[0];
			ConstantExpression c2 = (ConstantExpression)expression.Arguments[1];
			if (c1.Value is Type)
			{
				// We got the method where the user can define the persistent class via its type.
				TypeMapping mapping = TypeMapping.GetTypeMapping((Type)c1.Value);
				return string.Format("{0}.{1}", args.Storage.GetValidEntityName(mapping.PersistentAttribute.Entity), args.Storage.GetValidFieldName((string)c2.Value));
			}
			else
				return string.Format("{0}.{1}", args.Storage.GetValidEntityName((string)c1.Value), args.Storage.GetValidFieldName((string)c2.Value));
		}

		/// <summary>
		/// Invokes the new expression to return an instance of the object that is created in the expression.
		/// </summary>
		/// <param name="expression">The expression to be invoked.</param>
		private static T InvokeNewExpression<T>(NewExpression expression)
		{
			if (expression == null)
				throw new ArgumentNullException("expression");

			// Creates the list of arguments.
			object[] args = new object[expression.Arguments.Count];
			for(int i = 0; i < expression.Arguments.Count; i++)
			{
				args[i] = ((ConstantExpression)expression.Arguments[i]).Value;
			}

			// Creates the instance.
			return (T)expression.Constructor.Invoke(args);
		}

		/// <summary>
		/// Creates the parameters from a given array.
		/// </summary>
		/// <param name="array">The array that is translated into parameters.</param>
		/// <param name="args">The arguments that are passed in.</param>
		/// <returns></returns>
		private static string CreateParametersFromArray(Array array, MethodHandlerArgs args)
		{
			if (array == null)
				throw new ArgumentNullException("array");

			StringBuilder result = new StringBuilder();
			
			// compile the parameters.
			result.Append("(");

			int i = 0;
			// Loop over all items and add them to the builder.
			foreach (object v in array)
			{
				if (i > 0)
					result.Append(", ");
				result.Append(args.AddParameter(v, v.GetType()));
				i++;
			}

			return result.Append(")").ToString();
		}
	}
}
